Passed
Push — feature/improve-applicant-list ( 3fd796...cbd310 )
by Chris
04:06
created

applicationHooks.tsx ➔ useCriteria   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
1
/* eslint-disable camelcase */
2
import { useCallback, useEffect, useState } from "react";
3
import { useSelector } from "react-redux";
4
import { DispatchType } from "../configureStore";
5
import { RootState } from "../store/store";
6
import { getAwardRecipientTypes as fetchAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeActions";
7
import { getAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeSelector";
8
import { getAwardRecognitionTypes as fetchAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeActions";
9
import { getAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeSelector";
10
import { getEducationTypes as fetchEducationTypes } from "../store/EducationType/educationTypeActions";
11
import { getEducationTypes } from "../store/EducationType/educationTypeSelector";
12
import { getEducationStatuses as fetchEducationStatuses } from "../store/EducationStatus/educationStatusActions";
13
import { getEducationStatuses } from "../store/EducationStatus/educationStatusSelector";
14
import {
15
  AwardRecipientType,
16
  AwardRecognitionType,
17
  EducationType,
18
  EducationStatus,
19
  Job,
20
  Experience as ExperienceType,
21
  Skill,
22
  ExperienceSkill,
23
  ApplicationNormalized,
24
  Criteria,
25
  JobPosterQuestion,
26
  JobApplicationAnswer,
27
  Application,
28
  User,
29
} from "../models/types";
30
import {
31
  getApplicationById,
32
  getApplicationIsUpdating,
33
  getApplicationNormalized,
34
  getJobApplicationAnswers,
35
  getJobApplicationSteps,
36
  getStepsAreUpdating,
37
  getApplicationsByJob,
38
  isFetchingApplications,
39
} from "../store/Application/applicationSelector";
40
import {
41
  fetchApplication,
42
  touchApplicationStep,
43
  fetchApplicationsForJob,
44
} from "../store/Application/applicationActions";
45
import {
46
  getCriteriaByJob,
47
  getJob,
48
  getJobIsUpdating,
49
  getJobPosterQuestionsByJob,
50
} from "../store/Job/jobSelector";
51
import { fetchJob } from "../store/Job/jobActions";
52
import {
53
  ApplicationStatusId,
54
  ApplicationStep,
55
  ApplicationStepId,
56
  ProgressBarStatus,
57
} from "../models/lookupConstants";
58
import {
59
  getExperienceByApplicant,
60
  getExperienceByApplication,
61
  getExperienceSkillsByApplicant,
62
  getExperienceSkillsByApplication,
63
  getUpdatingByApplicant,
64
  getUpdatingByApplication,
65
} from "../store/Experience/experienceSelector";
66
import {
67
  fetchExperienceByApplicant,
68
  fetchExperienceByApplication,
69
} from "../store/Experience/experienceActions";
70
import { getSkills, getSkillsUpdating } from "../store/Skill/skillSelector";
71
import { fetchSkills } from "../store/Skill/skillActions";
72
import { getUserById } from "../store/User/userSelector";
73
import { fetchUser } from "../store/User/userActions";
74
75
export function useUser(userId: number | undefined): User | null {
76
  return useSelector((state: RootState) =>
77
    userId ? getUserById(state, { userId }) : null,
78
  );
79
}
80
export function useApplication(
81
  applicationId: number,
82
): ApplicationNormalized | null {
83
  return useSelector((state: RootState) =>
84
    getApplicationNormalized(state, { applicationId }),
85
  );
86
}
87
88
export function useReviewedApplication(
89
  applicationId: number,
90
): Application | null {
91
  return useSelector((state: RootState) =>
92
    getApplicationById(state, { id: applicationId }),
93
  );
94
}
95
96
export function useJob(jobId: number | undefined): Job | null {
97
  return useSelector((state: RootState) =>
98
    jobId ? getJob(state, { jobId }) : null,
99
  );
100
}
101
102
export function useExperienceConstants(): {
103
  awardRecipientTypes: AwardRecipientType[];
104
  awardRecognitionTypes: AwardRecognitionType[];
105
  educationTypes: EducationType[];
106
  educationStatuses: EducationStatus[];
107
} {
108
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
109
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
110
  const educationTypes = useSelector(getEducationTypes);
111
  const educationStatuses = useSelector(getEducationStatuses);
112
  return {
113
    awardRecipientTypes,
114
    awardRecognitionTypes,
115
    educationTypes,
116
    educationStatuses,
117
  };
118
}
119
120
export function useSkills(): Skill[] {
121
  return useSelector(getSkills);
122
}
123
124
export function useCriteria(jobId: number | undefined): Criteria[] {
125
  return useSelector((state: RootState) =>
126
    jobId ? getCriteriaByJob(state, { jobId }) : [],
127
  );
128
}
129
130
export function useExperiences(
131
  applicationId: number,
132
  application: ApplicationNormalized | null,
133
): ExperienceType[] {
134
  const applicantId = application?.applicant_id ?? 0;
135
136
  // When an Application is still a draft, use Experiences associated with the applicant profile.
137
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
138
  const useProfileExperience =
139
    application === null ||
140
    application.application_status_id === ApplicationStatusId.draft;
141
142
  // This selector must be memoized because getExperienceByApplicant/Application uses reselect, and not re-reselect, so it needs to preserve its state.
143
  const experienceSelector = useCallback(
144
    (state: RootState) =>
145
      useProfileExperience
146
        ? getExperienceByApplicant(state, { applicantId })
147
        : getExperienceByApplication(state, { applicationId }),
148
    [applicationId, applicantId, useProfileExperience],
149
  );
150
  const experiencesByType = useSelector(experienceSelector);
151
  const experiences: ExperienceType[] = [
152
    ...experiencesByType.award,
153
    ...experiencesByType.community,
154
    ...experiencesByType.education,
155
    ...experiencesByType.personal,
156
    ...experiencesByType.work,
157
  ];
158
  return experiences;
159
}
160
161
export function useExperienceSkills(
162
  applicationId: number,
163
  application: ApplicationNormalized | null,
164
): ExperienceSkill[] {
165
  // ExperienceSkills don't need to be fetched because they are returned in the Experiences API calls.
166
  const applicantId = application?.applicant_id ?? 0;
167
  const useProfileExperience =
168
    application === null ||
169
    application.application_status_id === ApplicationStatusId.draft;
170
  const expSkillSelector = (state: RootState) =>
171
    useProfileExperience
172
      ? getExperienceSkillsByApplicant(state, { applicantId })
173
      : getExperienceSkillsByApplication(state, { applicationId });
174
  const experienceSkills = useSelector(expSkillSelector);
175
  return experienceSkills;
176
}
177
178
export function useJobPosterQuestions(
179
  jobId: number | undefined,
180
): JobPosterQuestion[] {
181
  return useSelector((state: RootState) =>
182
    jobId ? getJobPosterQuestionsByJob(state, { jobId }) : [],
183
  );
184
}
185
186
export function useJobApplicationAnswers(
187
  applicationId: number,
188
): JobApplicationAnswer[] {
189
  return useSelector((state: RootState) =>
190
    getJobApplicationAnswers(state, { applicationId }),
191
  );
192
}
193
194
export function useApplicationUser(applicationId: number): User | null {
195
  const application = useApplication(applicationId);
196
  const user = application?.applicant?.user ?? null;
197
  return user;
198
}
199
200
/**
201
 * Return all skills from the redux store, and fetch the skills from backend if they are not yet in the store.
202
 * @param dispatch
203
 */
204
export function useFetchSkills(dispatch: DispatchType): Skill[] {
205
  const skills = useSelector(getSkills);
206
  const skillsUpdating = useSelector(getSkillsUpdating);
207
  useEffect(() => {
208
    if (skills.length === 0 && !skillsUpdating) {
209
      dispatch(fetchSkills());
210
    }
211
  }, [skills.length, skillsUpdating, dispatch]);
212
  return skills;
213
}
214
215
/**
216
 * Return all Experience constants from the redux store, and fetch them from backend if they are not yet in the store.
217
 * @param dispatch
218
 */
219
export function useFetchExperienceConstants(
220
  dispatch: DispatchType,
221
): {
222
  awardRecipientTypes: AwardRecipientType[];
223
  awardRecognitionTypes: AwardRecognitionType[];
224
  educationTypes: EducationType[];
225
  educationStatuses: EducationStatus[];
226
} {
227
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
228
  const awardRecipientTypesLoading = useSelector(
229
    (state: RootState) => state.awardRecipientType.loading,
230
  );
231
  useEffect(() => {
232
    if (awardRecipientTypes.length === 0 && !awardRecipientTypesLoading) {
233
      dispatch(fetchAwardRecipientTypes());
234
    }
235
  }, [awardRecipientTypes, awardRecipientTypesLoading, dispatch]);
236
237
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
238
  const awardRecognitionTypesLoading = useSelector(
239
    (state: RootState) => state.awardRecognitionType.loading,
240
  );
241
  useEffect(() => {
242
    if (awardRecognitionTypes.length === 0 && !awardRecognitionTypesLoading) {
243
      dispatch(fetchAwardRecognitionTypes());
244
    }
245
  }, [awardRecognitionTypes, awardRecognitionTypesLoading, dispatch]);
246
247
  const educationTypes = useSelector(getEducationTypes);
248
  const educationTypesLoading = useSelector(
249
    (state: RootState) => state.educationType.loading,
250
  );
251
  useEffect(() => {
252
    if (educationTypes.length === 0 && !educationTypesLoading) {
253
      dispatch(fetchEducationTypes());
254
    }
255
  }, [educationTypes, educationTypesLoading, dispatch]);
256
257
  const educationStatuses = useSelector(getEducationStatuses);
258
  const educationStatusesLoading = useSelector(
259
    (state: RootState) => state.educationStatus.loading,
260
  );
261
  useEffect(() => {
262
    if (educationStatuses.length === 0 && !educationStatusesLoading) {
263
      dispatch(fetchEducationStatuses());
264
    }
265
  }, [educationStatuses, educationStatusesLoading, dispatch]);
266
267
  return {
268
    awardRecipientTypes,
269
    awardRecognitionTypes,
270
    educationTypes,
271
    educationStatuses,
272
  };
273
}
274
275
export function useJobApplicationSteps(): {
276
  [step in ApplicationStep]: ProgressBarStatus;
277
} {
278
  return useSelector(getJobApplicationSteps);
279
}
280
281
/**
282
 * Dispatches an api request that will record the step as touched, setting it to "complete" or "error",
283
 * and gets the validation status of the application steps.
284
 *
285
 * Returns true if the request is currently in progress, false otherwise.
286
 *
287
 * NOTE: this hook only runs once, when the component is first mounted.
288
 */
289
export function useTouchApplicationStep(
290
  applicationId: number,
291
  step: ApplicationStep,
292
  dispatch: DispatchType,
293
): boolean {
294
  const stepsAreUpdating = useSelector(getStepsAreUpdating);
295
  useEffect(() => {
296
    dispatch(touchApplicationStep(applicationId, ApplicationStepId[step]));
297
  }, [applicationId, step, dispatch]);
298
  return stepsAreUpdating;
299
}
300
301
/**
302
 * Return an Application (normalized, ie without Review) from the redux store, and fetch it from backend if it is not yet in the store.
303
 * @param applicationId
304
 * @param dispatch
305
 */
306
export function useFetchNormalizedApplication(
307
  applicationId: number,
308
  dispatch: DispatchType,
309
): ApplicationNormalized | null {
310
  const applicationSelector = (
311
    state: RootState,
312
  ): ApplicationNormalized | null =>
313
    getApplicationNormalized(state, { applicationId });
314
  const application: ApplicationNormalized | null = useSelector(
315
    applicationSelector,
316
  );
317
  const applicationIsUpdating = useSelector((state: RootState) =>
318
    getApplicationIsUpdating(state, { applicationId }),
319
  );
320
  const [applicationFetched, setApplicationFetched] = useState(false);
321
  useEffect(() => {
322
    if (application === null && !applicationIsUpdating && !applicationFetched) {
323
      setApplicationFetched(true);
324
      dispatch(fetchApplication(applicationId));
325
    }
326
  }, [application, applicationId, applicationIsUpdating, dispatch]);
327
  return application;
328
}
329
330
/**
331
 * Return an Application from the redux store, and fetch it from backend if it is not yet in the store.
332
 * @param applicationId
333
 * @param dispatch
334
 */
335
export function useFetchApplication(
336
  applicationId: number,
337
  dispatch: DispatchType,
338
): Application | null {
339
  const applicationSelector = (state: RootState): Application | null =>
340
    getApplicationById(state, { id: applicationId });
341
  const application: Application | null = useSelector(applicationSelector);
342
  const applicationIsUpdating = useSelector((state: RootState) =>
343
    getApplicationIsUpdating(state, { applicationId }),
344
  );
345
  useEffect(() => {
346
    if (application === null && !applicationIsUpdating) {
347
      dispatch(fetchApplication(applicationId));
348
    }
349
  }, [application, applicationId, applicationIsUpdating, dispatch]);
350
  return application;
351
}
352
353
/**
354
 * Return an array of Applications related to a given Job ID from the redux store.
355
 * Fetch them from the backend if they are not yet in the store.
356
 * @param jobId
357
 * @param dispatch
358
 */
359
export function useFetchApplicationsByJob(
360
  jobId: number,
361
  dispatch: DispatchType,
362
): Application[] | null {
363
  const applications = useSelector((state: RootState) =>
364
    getApplicationsByJob(state, { jobId }),
365
  );
366
  const applicationsAreFetching = useSelector(isFetchingApplications);
367
  useEffect(() => {
368
    if (applications.length === 0 && !applicationsAreFetching) {
369
      dispatch(fetchApplicationsForJob(jobId));
370
    }
371
  }, [applications, jobId, applicationsAreFetching, dispatch]);
372
  return applications;
373
}
374
375
/**
376
 * Return a Job from the redux store, and fetch it from backend if it is not yet in the store.
377
 * @param jobId
378
 * @param dispatch
379
 */
380
export function useFetchJob(
381
  jobId: number | undefined,
382
  dispatch: DispatchType,
383
): Job | null {
384
  const job = useJob(jobId);
385
  const jobUpdatingSelector = (state: RootState) =>
386
    jobId ? getJobIsUpdating(state, jobId) : false;
387
  const jobIsUpdating = useSelector(jobUpdatingSelector);
388
  useEffect(() => {
389
    // If job is null and not already updating, fetch it.
390
    if (jobId && job === null && !jobIsUpdating) {
391
      dispatch(fetchJob(jobId));
392
    }
393
  }, [jobId, job, jobIsUpdating, dispatch]);
394
  return job;
395
}
396
397
/**
398
 * Return all Experience relevant to an Application from the redux store, and fetch it from backend if it is not yet in the store.
399
 * @param applicationId
400
 * @param application
401
 * @param dispatch
402
 */
403
export function useFetchExperience(
404
  applicationId: number,
405
  application: ApplicationNormalized | null,
406
  dispatch: DispatchType,
407
): {
408
  experiences: ExperienceType[];
409
  experiencesUpdating: boolean;
410
  experiencesFetched: boolean;
411
} {
412
  const applicantId = application?.applicant_id ?? 0;
413
414
  // When an Application is still a draft, use Experiences associated with the applicant profile.
415
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
416
  const applicationLoaded = application !== null;
417
  const useProfileExperience =
418
    application === null ||
419
    application.application_status_id === ApplicationStatusId.draft;
420
421
  const experiences = useExperiences(applicationId, application);
422
  const experiencesUpdating = useSelector((state: RootState) =>
423
    useProfileExperience
424
      ? getUpdatingByApplicant(state, { applicantId })
425
      : getUpdatingByApplication(state, { applicationId }),
426
  );
427
  const [experiencesFetched, setExperiencesFetched] = useState(false);
428
  useEffect(() => {
429
    // Only load experiences if they have never been fetched by this component (!experiencesFetched),
430
    //  have never been fetched by another component (length === 0),
431
    //  and are not currently being fetched (!experiencesUpdating).
432
    // Also, wait until application has been loaded so the correct source can be determined.
433
    if (
434
      applicationLoaded &&
435
      !experiencesFetched &&
436
      !experiencesUpdating &&
437
      experiences.length === 0
438
    ) {
439
      setExperiencesFetched(true);
440
      if (useProfileExperience) {
441
        dispatch(fetchExperienceByApplicant(applicantId));
442
      } else {
443
        dispatch(fetchExperienceByApplication(applicationId));
444
      }
445
    }
446
  }, [
447
    applicantId,
448
    applicationId,
449
    applicationLoaded,
450
    dispatch,
451
    experiences.length,
452
    experiencesFetched,
453
    experiencesUpdating,
454
    useProfileExperience,
455
  ]);
456
  return {
457
    experiences,
458
    experiencesUpdating,
459
    experiencesFetched,
460
  };
461
}
462
463
/**
464
 * Return an User from the redux store, and fetch it from backend if it is not yet in the store.
465
 * @param jobId
466
 * @param dispatch
467
 */
468
export function useFetchUser(
469
  userId: number,
470
  dispatch: DispatchType,
471
): User | null {
472
  const user = useUser(userId);
473
  useEffect(() => {
474
    // If job is null and not already updating, fetch it.
475
    if (userId) {
476
      dispatch(fetchUser(userId));
477
    }
478
  }, [userId, dispatch]);
479
  return user;
480
}
481
482
/**
483
 * Trigger fetches for all data needed for the Application process which is not yet in the redux store, or in the process of loading.
484
 * @param applicationId
485
 */
486
export function useFetchAllApplicationData(
487
  applicationId: number,
488
  dispatch: DispatchType,
489
): {
490
  applicationLoaded: boolean;
491
  userLoaded: boolean;
492
  jobLoaded: boolean;
493
  criteriaLoaded: boolean;
494
  experiencesLoaded: boolean;
495
  experienceSkillsLoaded: boolean;
496
  jobQuestionsLoaded: boolean;
497
  applicationAnswersLoaded: boolean;
498
  experienceConstantsLoaded: boolean;
499
  skillsLoaded: boolean;
500
} {
501
  const application = useFetchNormalizedApplication(applicationId, dispatch);
502
  const jobId = application?.job_poster_id;
503
  const job = useFetchJob(jobId, dispatch);
504
  const { experiences, experiencesUpdating } = useFetchExperience(
505
    applicationId,
506
    application,
507
    dispatch,
508
  );
509
  const {
510
    awardRecipientTypes,
511
    awardRecognitionTypes,
512
    educationTypes,
513
    educationStatuses,
514
  } = useFetchExperienceConstants(dispatch);
515
  const skills = useFetchSkills(dispatch);
516
517
  const applicationLoaded = application !== null;
518
  const jobLoaded = job !== null;
519
  const experiencesLoaded = !experiencesUpdating || experiences.length > 0;
520
  const experienceConstantsLoaded =
521
    awardRecipientTypes.length > 0 &&
522
    awardRecognitionTypes.length > 0 &&
523
    educationTypes.length > 0 &&
524
    educationStatuses.length > 0;
525
  const skillsLoaded = skills.length > 0;
526
527
  return {
528
    applicationLoaded,
529
    jobLoaded,
530
    experiencesLoaded,
531
    experienceConstantsLoaded,
532
    skillsLoaded,
533
    criteriaLoaded: jobLoaded,
534
    experienceSkillsLoaded: experiencesLoaded,
535
    jobQuestionsLoaded: jobLoaded,
536
    applicationAnswersLoaded: applicationLoaded,
537
    userLoaded: applicationLoaded,
538
  };
539
}
540
541
/**
542
 * Trigger fetches for all data needed for the Application review process which is not yet in the redux store, or in the process of loading.
543
 * @param applicationId
544
 */
545
export function useFetchReviewApplicationData(
546
  applicantUserId: number,
547
  applicationId: number,
548
  jobId: number,
549
  dispatch: DispatchType,
550
): {
551
  applicationLoaded: boolean;
552
  jobLoaded: boolean;
553
  experiencesLoaded: boolean;
554
  experienceConstantsLoaded: boolean;
555
  skillsLoaded: boolean;
556
} {
557
  const application = useFetchApplication(applicationId, dispatch);
558
  const job = useFetchJob(jobId, dispatch);
559
  const { experiences, experiencesUpdating } = useFetchExperience(
560
    applicationId,
561
    application,
562
    dispatch,
563
  );
564
  const {
565
    awardRecipientTypes,
566
    awardRecognitionTypes,
567
    educationTypes,
568
    educationStatuses,
569
  } = useFetchExperienceConstants(dispatch);
570
  const skills = useFetchSkills(dispatch);
571
572
  return {
573
    applicationLoaded: application !== null,
574
    jobLoaded: job !== null,
575
    experiencesLoaded: !experiencesUpdating || experiences.length > 0,
576
    experienceConstantsLoaded:
577
      awardRecipientTypes.length > 0 &&
578
      awardRecognitionTypes.length > 0 &&
579
      educationTypes.length > 0 &&
580
      educationStatuses.length > 0,
581
    skillsLoaded: skills.length > 0,
582
  };
583
}
584